<?php
/**
 * Moodle formulas question type class.
 *
 * @copyright &copy; 2010 Hon Wai, Lau
 * @author Hon Wai, Lau <lau65536@gmail.com>
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License version 3
 * @package questionbank
 * @subpackage questiontypes
 */

require_once("$CFG->dirroot/question/type/questiontype.php");
require_once("$CFG->dirroot/question/type/formulas/variables.php");
require_once("$CFG->dirroot/question/type/formulas/answer_unit.php");
require_once("$CFG->dirroot/question/type/formulas/conversion_rules.php");

/**
 * The formulas question class
 *
 * TODO give an overview of how the class works here.
 */
class question_formulas_qtype extends default_questiontype {

    function name() {
        return 'formulas';
    }
    
    
    /// return the tags of subquestion answer field of the database/variable
    function subquestion_answer_tags() {
        return array('placeholder','answermark','answertype','numbox','vars1','answer','vars2','correctness'
            ,'unitpenalty','postunit','ruleid','otherrule','trialmarkseq','subqtext','feedback');
    }
    
    
    /// return the extra options field of the formulas question type
    function subquestion_option_extras() {
        return array('varsrandom', 'varsglobal', 'peranswersubmit', 'showperanswermark');
    }
    
    
    /// Attempt to get the options in the database, return non-zero value if fail
    function get_question_options_part(&$question) {
        if (!$question->options->extra = get_record('question_formulas', 'questionid', $question->id))  return 1;
        if (!$question->options->answers = get_records('question_formulas_answers', 'questionid', $question->id, 'id ASC'))  return 2;
        if (count($question->options->answers) == 0)  return 3; // It must have at least one answer
        $question->options->answers = array_values($question->options->answers);
        return 0;
    }
    
    
    /// All the additional datum generated for this question type are stored in $question->options
    function get_question_options(&$question) {
        try {
            if ( ($errcode = $this->get_question_options_part($question)) != 0)
                throw new Exception('Error: Missing formulas question options for questionid ' . $question->id . '. Error ' . $errcode);
            $question->options->numpart = count($question->options->answers);
            foreach ($question->options->answers as $idx => $part) {
                $part->location = $idx;     // it is useful when we only pass the parameter $part, the location stores which part is it
                $part->fraction = 1;        // used by get_all_responses()
            }
        } catch (Exception $e) {
            notify($e->getMessage());
            return false;
        }
        return true;
    }
    
    
    /// Attempt to insert or update a record in the database. May throw error
    function question_options_insertdb($dbname, &$record, $oldid) {
        if (isset($oldid)) {
            $record->id = $oldid;    // if there is old id, reuse it.
            if (!update_record($dbname, $record))
                throw new Exception("Could not update quiz record in database $dbname! (id=$oldid)");
        }
        else {
            if (!$record->id = insert_record($dbname, $record))
                throw new Exception("Could not insert quiz record in database $dbname! (id=$oldid)");
        }
    }


    /// Save the varsdef, answers and units to the database tables from the editing form
    function save_question_options($question) {
        $errcode = $this->get_question_options_part($question); // get old options from the database, id will be reused if it exist
        $oldextra = ($errcode == 0 || $errcode >= 2) ? $question->options->extra : null;
        $oldanswers = ($errcode == 0) ? $question->options->answers : null;
        
        try {
            $filtered = $this->validate($question); // data from the web input interface should be validated
            if (count($filtered->errors) > 0)   // there may be error from import or restore
                throw new Exception('Format error! Probably import/restore files have been damaged.');
            $ss = $this->create_subquestion_structure($question->questiontext, $filtered->answers);
            // reorder the answer, so that the ordering of answers is the same as the placeholder ordering in the main text
            foreach ($ss->answerorders as $newloc)  $newanswers[] = $filtered->answers[$newloc];
            
            $idcount = 0;
            foreach ($newanswers as $i=>$ans) {
                $this->question_options_insertdb('question_formulas_answers', $ans, isset($oldanswers[$idcount]) ? $oldanswers[$idcount++]->id : null);
                $newanswerids[$i] = $ans->id;
            }
            
            // delete remaining used records
            for ($i=count($newanswers); $i<count($oldanswers); ++$i)
                delete_records('question_formulas_answers', 'id', $oldanswers[$i]->id);
            
            $newextra = new stdClass;
            $newextra->questionid  = $question->id;
            $extras = $this->subquestion_option_extras();
            foreach ($extras as $extra)  $newextra->$extra = trim($question->$extra);
            $newextra->answerids   = implode(',',$newanswerids);
            $this->question_options_insertdb('question_formulas', $newextra, isset($oldextra) ? $oldextra->id : null);
        } catch (Exception $e) {
            return (object)array('error' => $e->getMessage());
        }
        
        return true;
    }


    /// Override the parent save_question in order to change the defaultgrade.
    function save_question($question, $form, $course) {
        $form->defaultgrade = array_sum($form->answermark); // the default grade is the total grade of its subquestion
        return parent::save_question($question, $form, $course);
    }


    /// Deletes question from the question-type specific tables with $questionid
    function delete_question($questionid) {
        delete_records('question_formulas', 'questionid', $questionid);
        delete_records('question_formulas_answers', 'questionid', $questionid);
        return true;
    }
    
    
    
    
    
    /// Parses the variable texts and then generates a random set of variables for this session
    function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
        /// The default $state->responses[""] is not used, instead, the following field are created
        /// responses["subanum"] represents which answer is submitted, -1 means all answer are submitted at once
        /// The number before "_" is the subquestion number, the number after "_" is the location of the "coordinate" within that subquestion
        /// e.g. responses["0_0"], responses["0_1"], responses["0_2"], ... Note that the last one for the same subquestion represents a unit
        /// The responses["0_"], maybe received from students which will be split and stored in responses["0_0"], responses["0_1"]
        /// The grading results are stored in the array: trials, raw_grades, anscorrs, unitcorrs and fractions
        /// Note that fractions is not stored in the database because it can be always be calculated from anscorrs and unitcorrs
        try {
            $vars = new question_formulas_variables(); // store the random variable
            $vars->parse_random_variables($question->options->extra->varsrandom);
            $vars->instantiate_random_variables(); // instantiate a set of random variable
            $state->randomvars = $vars->get_variables();
            $state->globalvars_text = $question->options->extra->varsglobal;
            $state->trials = array_fill(0, $question->options->numpart, 0);
            $state->raw_grades = array_fill(0, $question->options->numpart, 0);
            $state->fractions = array_fill(0, $question->options->numpart, 0);
            $state->anscorrs = array_fill(0, $question->options->numpart, 0);
            $state->unitcorrs = array_fill(0, $question->options->numpart, 0);
            foreach ($question->options->answers as $i => $part)  foreach (range(0,$part->numbox) as $j)
                $state->responses["${i}_$j"] = '';  // fill all response by empty string
            $state->responses['subanum'] = '';  // It has no meaning before the students submit the first answer.
            return true;    // success
        } catch (Exception $e) {
            return false;   // fail
        }
    }


    /// Restore the variables and answers from the last session
    function restore_session_and_responses(&$question, &$state) {
        try {
            $lines = explode("\n", $state->responses['']);
            $counter = 0;
            $details = array();
            while (strlen($lines[$counter]) != 0) { // read lines until a empty one is encountered
                $pair = explode('=', $lines[$counter], 2);
                if (count($pair) == 2)  $details[$pair[0]] = $pair[1];  // skip lines without "=", if any
                $counter++;
            }
            foreach ($question->options->answers as $i => $part) {
                $grading = array_key_exists($i, $details) ? explode(',', $details[$i]) : array(0,0,0,0);
                list($state->trials[$i],$state->raw_grades[$i],$state->anscorrs[$i],$state->unitcorrs[$i]) = $grading;
                $state->fractions[$i] = $state->anscorrs[$i] * ($state->unitcorrs[$i] ? 1 : (1-$part->unitpenalty));   // recalculate mark fraction
                foreach (range(0,$part->numbox) as $j)
                    $state->responses["${i}_$j"] = array_key_exists("${i}_$j", $details) ? $details["${i}_$j"] : '';
            }
            $state->responses['subanum'] = intval($lines[$counter+1]);
            $vars = new question_formulas_variables();  // the remaining line is a simple (variable => value) pairs
            $vars->evaluate_additional_variables(implode("",array_slice($lines,$counter+3)));
            $state->randomvars = $vars->get_variables();
            $state->globalvars_text = $question->options->extra->varsglobal;
            
            $state->responses[''] = '';     // remove this info for clear display in the item analysis
            return true;    // success
        } catch (Exception $e) {
            notify("Error: Session record is either damaged or inconsistent with the question (#{$question->id}).");
            return false;   // fail
        }
    }
    
    
    /// The first line stores the variables and the following lines store the responses for each subquestions
    function save_session_and_responses(&$question, &$state) {
        $responses_str = '';
        foreach ($question->options->answers as $i => $part) {
            if ($state->trials[$i] > 0) // if the subquestion has been tried, store the number of trial and grading result
                $responses_str .= ($i . '=' . $state->trials[$i].','.$state->raw_grades[$i].','.$state->anscorrs[$i].','.$state->unitcorrs[$i] . "\n");
            foreach (range(0,$part->numbox) as $j) { // store all none-empty responses for each input location
                $ex = explode("\n", isset($state->responses["${i}_$j"]) ? $state->responses["${i}_$j"] : '');
                if (strlen($ex[0]) > 0 )  $responses_str .= "${i}_$j" . '=' . $ex[0] . "\n";  // use less database space, if empty
            }
        }
        $responses_str .= "\n";  // signify the end of the above responses region
        $responses_str .= $state->responses['subanum'] . "\n"; // this line is submission info, started by submitted answer number
        $responses_str .= "\n";  // reserve an empty line here for later use...
        $vars = new question_formulas_variables($state->randomvars);
        $responses_str .= $vars->get_serialization() . "\n"; // the remaining part line is the random variables of the session
        
        // Set the legacy answer field
        if (!set_field('question_states', 'answer', $responses_str, 'id', $state->id)) {
            return false;
        }
        return true;
    }
    
    
    
    
    
    /// Replace variables and format the text before print it out
    function get_substituted_question_texts($question, $cmoptions, $vars, $text, $class_names) {
        if (strlen(trim($text)) == 0)  return '';
        $subtext = $vars->substitute_variables_in_text($text);
        $restext = $this->format_text($subtext, $question->questiontextformat, $cmoptions);
        return ($class_names === '') ? $restext : '<div class="'.$class_names.'">'.$restext.'</div>'."\n";
    }
    
    
    /// Print the marks for each answer, if the showperanswermark is checked
    function get_per_answer_mark(&$question, $state, $cmoptions, $options, $i) {
        if ($question->options->extra->showperanswermark) {
            $maxmark = $question->options->answers[$i]->answermark; // there is no scaling of grade...
            if ($state->last_graded->trials[$i] < 1)    // if not graded, display -/mark
                return '<span class="grade formulas_mark" title="Not graded yet"> Mark: -/'.$maxmark.'</span>';
            else {
                $resstr = '<span class="grade formulas_mark"';
                if ($cmoptions->penaltyscheme) {
                    $tmf = $this->get_trial_mark_fraction($question->options->answers[$i], $state->last_graded->trials[$i]);
                    $resstr .= ' title="Submission #'.$state->last_graded->trials[$i]
                        .', result: '.($maxmark*$state->last_graded->fractions[$i]*$tmf[0]).'/'.$maxmark
                        .' (Raw mark: '.($maxmark*$state->last_graded->fractions[$i])
                        .($state->last_graded->unitcorrs[$i]? '' : ', Wrong unit')
                        .', Maximum mark: '.($tmf[0]*100).'%'.')"';
                }
                return $resstr . '> Mark: '.$state->last_graded->raw_grades[$i].'/'.$maxmark.'</span>';
            }
        }
    }
    
    
    /// Get next few submission information that will be shown when the mouse hovering over the submit button
    function get_submission_info($question, $state, $cmoptions, $options, $i) {
        $percents = array();
        $n = 5;
        $cur_trial = $state->last_graded->trials[$i];
        for ($z = 0; $z < $n; $z++) {
            $tmf = $this->get_trial_mark_fraction($question->options->answers[$i], $cur_trial+1+$z);
            $max_trial = $tmf[1];
            if ($cur_trial+$z < $max_trial || $max_trial < 0)  $percents[] = round($tmf[0]*100,1).'%';
        }
        return 'Mark for next few submissions: ' . implode(', ', $percents)
            .($max_trial<0 ? ' (Trial unlimited)' : ($cur_trial+$n < $max_trial ? ', ... ('.($max_trial-$cur_trial-$n).' more)' : ''));
    }
    
    
    /// return the html for the submit button
    function get_per_answer_submit_button($question, $state, $cmoptions, $options, $i, $submit_location_id, $subans_track_id) {
        if ($question->options->extra->peranswersubmit && ($cmoptions->optionflags & QUESTION_ADAPTIVE) && !$options->readonly) {
            if ($cmoptions->penaltyscheme) {
                $tmf = $this->get_trial_mark_fraction($question->options->answers[$i], $state->last_graded->trials[$i]+1);
                $info = $this->get_submission_info($question, $state, $cmoptions, $options, $i);
                if ($tmf[0] < 0)  return '';
                return '<span title="'.$info.'"><input type="button" class="btn formulas_submit" value="Submit" onclick="'
                    ."formulas_submit('$submit_location_id','$submit_location_id','$subans_track_id','$i','{$question->id}')".'"></span>';
            }
        }
        return '';
    }
    
    
    function get_subquestion_formulation($question, $state, $cmoptions, $options, $i, $showfeedback) {
        $part = &$question->options->answers[$i];
        $vars = $this->get_local_variables($part, &$state);
        $tmf = $this->get_trial_mark_fraction($part, $state->last_graded->trials[$i]+1);
        $readonly = (!empty($options->readonly) || $tmf[0] < 0) ? 'disabled="disabled"' : ''; // disable if readonly or no more trial
        $feedbackclass = $showfeedback ? question_get_feedback_class($state->fractions[$i]) : '';
        $unitfeedbackclass = $showfeedback ? question_get_feedback_class($state->unitcorrs[$i]) : '';
        $boxfeedbackclass = $showfeedback ? question_get_feedback_class($state->anscorrs[$i]) : '';
        $feedback_image = $showfeedback ? question_get_feedback_image($state->fractions[$i]) : '';
        $subqreplaced = $this->get_substituted_question_texts($question, $cmoptions, $vars, $part->subqtext, '');
        
        if ($part->numbox == 1 && strlen($subqreplaced) == 0)  $subqreplaced = '{_0}{_u}';

        // get the set of defined placeholder and its options, also missing placeholder are appended at the end
        $pattern = '\{(_[0-9u][0-9]*)(:[^{}]+)?\}';
        preg_match_all('/'.$pattern.'/', $subqreplaced, $matches);
        $boxes = array();
        foreach ($matches[1] as $j => $match)  if (!array_key_exists($match, $boxes))   // if there is duplication, it will be skipped
            $boxes[$match] = (object)array('pattern' => $matches[0][$j], 'options' => $matches[2][$j]);
        foreach (range(0, $part->numbox) as $j => $notused) {
            $placeholder = ($j == $part->numbox) ? "_u" : "_$j";
            if (!array_key_exists($placeholder,$boxes)) {
                $boxes[$placeholder] = (object)array('pattern' => "{".$placeholder."}", 'options' => '');
                $subqreplaced .= "{".$placeholder."}";  // appended at the end
            }
        }
        
        // Add the location of the submit button and mark information at the end, if user has not used them
        if (strpos($subqreplaced, '{_s}') === false)  $subqreplaced .= '{_s}';
        if (strpos($subqreplaced, '{_m}') === false)  $subqreplaced .= '{_m}';
        
        // if {_0} and {_u} are adjacent to each other and there is only one number in the answer, "concatenate" them together into one input box
        if ($part->numbox == 1 && strpos($subqreplaced, "{_0}{_u}") !== false) {
            $inputbox = '<input type="text" class="formulas_number_unit '.$feedbackclass.'" '.$readonly.' title="'
                .get_string('number','qtype_formulas').($part->postunit=='' ? '' : ' & '.get_string('unit','qtype_formulas')).'"'
                .' name="'.$question->name_prefix.$i."_".'"'
                .' value="'.$state->responses["${i}_0"].$state->responses["${i}_1"].'"/>';
            $subqreplaced = str_replace("{_0}{_u}", $inputbox, $subqreplaced);
        }
        
        // get the set of string for each candidate input box {_0}, {_1}, ..., {_u}
        $inputboxes = array();
        foreach (range(0,$part->numbox) as $j) {    // replace the input box for each placeholder {_0}, {_1} ...
            $placeholder = ($j == $part->numbox) ? "_u" : "_$j";    // the last one is unit
            $var_name = "${i}_$j";
            $name = $question->name_prefix.$var_name;
            $response = $state->responses[$var_name];
            
            if (strlen($boxes[$placeholder]->options) == 0) {
                $inputboxes[$placeholder] = '';
                if ($j == $part->numbox)    // check whether it is a unit placeholder
                    $inputboxes[$placeholder] = (strlen($part->postunit) == 0) ? '' :
                        '<input type="text" class="'.'formulas_unit '.$unitfeedbackclass.'" '.$readonly.' title="'
                        .get_string('unit','qtype_formulas').'"'.' name="'.$name.'" value="'.$response.'"/>';
                else
                    $inputboxes[$placeholder] = '<input type="text" class="'.'formulas_number '.$boxfeedbackclass.'" '.$readonly.' title="'
                        .get_string('number','qtype_formulas').'"'.' name="'.$name.'" value="'.$response.'"/>';
            }
            else { // MC, experimental features...
                $mctexts = $vars->get_variable(substr($boxes[$placeholder]->options,1));
                if (isset($mctexts) && is_array($mctexts)) {
                    $mc = '';
                    foreach ($mctexts as $x => $mctxt) {
                        $mc .= '<tr class="r'.($x%2).'"><td class="c0 control">';
                        $mc .= '<input id="'.$name.'_'.$x.'" name="'.$name.'" value="'.$x.'" type="radio" '.((string)$x==$response?' checked="checked" ':'').'>';
                        $mc .= '</td><td class="c1 text "><label for="'.$name.'_'.$x.'">'.$mctxt.'</label> '.((string)$x==$response?$feedback_image:'').'</td>';
                        $mc .= '</tr>';
                    }
                    $inputboxes[$placeholder] = '<table><tbody>' . $mc . '</tbody></table>';
                }
            }
        }
        
        // sequential replacement has the issue that the string such as {_0},... cannot be used in the MC, minor issue
        foreach ($inputboxes as $placeholder => $replacement)
            $subqreplaced = preg_replace('/'.$boxes[$placeholder]->pattern.'/', $replacement, $subqreplaced, 1);
        
        return $subqreplaced;
    }
    
    
    /// Print the question text and its subquestions answer box, give feedback if submitted.
    function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
        global $CFG;
        $submit_location_id = $question->name_prefix.'submit';    // it should have no same question on the same page (?)
        $subans_track_id = $question->name_prefix.'subanum';
        
        // -------------- print the image if any --------------
        if ($image = get_question_image($question, $cmoptions->course)) {
            echo '<img class="qimage" src="'.$image.'" alt="" /><br />'."\n";
        }
        
        // -------------- display question body --------------
        try {
            $globalvars = $this->get_global_variables(&$state);
            foreach ($question->options->answers as $i => $part) {
                $this->get_local_variables($part, &$state);
                $this->get_trial_mark_fraction($part, 0);
            }
        } catch (Exception $e) {
            notify("Error: Question evaluation failure, probably the question is changed and is not checked.");
            return;
        }
        $ss = $this->create_subquestion_structure($question->questiontext, $question->options->answers);
        if (count($ss->pretexts) != $question->options->numpart) {
            notify("Error: The number of subquestions and number of answer is not the same.");
            return;
        }
        foreach ($question->options->answers as $i => $part) {
            $showfeedback = $options->feedback && ($state->responses['subanum'] == -1 || $state->responses['subanum'] == $i);
            $pretext = $this->get_substituted_question_texts($question, $cmoptions, $globalvars, $ss->pretexts[$i], '');
            $feedback = '';//$showfeedback ? $this->get_substituted_question_texts($question, $cmoptions, $vars, $part->feedback, 'feedback') : '';
            $submit_button = $this->get_per_answer_submit_button($question, $state, $cmoptions, $options, $i, $submit_location_id, $subans_track_id);
            $mark_info = $this->get_per_answer_mark($question, $state, $cmoptions, $options, $i);
            $feedback_image = $showfeedback ? question_get_feedback_image($state->fractions[$i]) : '';
            
            $subqreplaced = $this->get_subquestion_formulation($question, $state, $cmoptions, $options, $i, $showfeedback);
            $subqreplaced = str_replace('{_s}', $submit_button, $subqreplaced);
            $subqreplaced = str_replace('{_m}', $mark_info.$feedback_image, $subqreplaced);
            echo $pretext . '<div class="formulas_subpart">' . $subqreplaced . '</div>' . $feedback;
        }
        echo $this->get_substituted_question_texts($question, $cmoptions, $globalvars, $ss->posttext, '');
        
        if ($question->options->extra->peranswersubmit == 1) {  // special parameter to store which submit answer
            echo '<input type="hidden" id="'.$subans_track_id.'" name="'.$subans_track_id.'" value="-1"/>';
            // The following placeholder allows the additional POST name (respid_submit) to trigger the grading process...
            echo '<div id="'.$submit_location_id.'" style="display:none""></div>';
        }
        else {
            echo '<br><span title="'.$this->get_submission_info($question, $state, $cmoptions, $options, $i).'">';
            $this->print_question_submit_buttons($question, $state, $cmoptions, $options);
            echo '</span><br>';
        }
    }





    /// given a particular trial $trial_number, return the pair of maximum mark fraction and maximum number of trials. Throw on parsing error
    function get_trial_mark_fraction(&$part, $trial_number) {
        if (!isset($part->trialmarkseq_parsed)) {   // reuse the results if it has been parsed before
            $mseq = explode(',', $part->trialmarkseq);
            if (!is_numeric($mseq[0]) || floatval($mseq[0]) != 1.0)
                throw new Exception(get_string('error_trial_mark_order','qtype_formulas'));
            array_unshift($mseq, 1.0);  // append one 1.0 (100%, full mark) for easy computation later.
            $part->trialmarkseq_loop = strlen(trim(end($mseq))) == 0;   // if it is ended with a comma, i.e. we want inifinite trial
            if ($part->trialmarkseq_loop)  array_pop($mseq);    // pop the last element because it is empty string
            foreach ($mseq as $i => $v)  if (is_numeric($v)) {
                $mseq[$i] = floatval($mseq[$i]);
                if (($i > 0 && $mseq[$i] > $mseq[$i-1]) || $mseq[$i]<0)
                    throw new Exception(get_string('error_trial_mark_order','qtype_formulas'));
            }
            else
                throw new Exception(get_string('error_trial_mark_nonnumeric','qtype_formulas'));
            $part->trialmarkseq_parsed = $mseq;
        }   // lazy evaluation, construct the trialmarkseq only when it is used somewhere
        $mseq = $part->trialmarkseq_parsed;
        
        if ($part->trialmarkseq_loop) { // different of the last two elements is being repeated
            if ($trial_number < count($mseq))  return array($mseq[$trial_number], -1);
            $repeat_penalty = $mseq[count($mseq)-2] - $mseq[count($mseq)-1];
            return array(round(max(0, $mseq[count($mseq)-1] - $repeat_penalty*($trial_number-count($mseq)+1)), 10), -1);
        }
        else {  // with finite trial
            if ($trial_number < count($mseq))  return array($mseq[$trial_number], count($mseq)-1);
            return array(-1, count($mseq)-1);  // -1 indicates no further submission is allowed
        }
    }
    
    
    /// return the object of question_formulas_variables() with the global variable text defined in the $part. May throw error
    function get_global_variables(&$state) {
        if (!isset($state->globalvars)) {   // Perform lazy evaluation
            $vars = new question_formulas_variables($state->randomvars);
            $vars->evaluate_additional_variables($state->globalvars_text);
            $state->globalvars = $vars->get_variables();    // global variable always include random variable
        }   // Perform the evaluation only when the global variable does not exist before
        return new question_formulas_variables($state->globalvars);
    }
    
    
    /// return the object of question_formulas_variables() with the local variable defined in the $part. May throw error
    function get_local_variables($part, &$state) {
        if (!isset($state->localvars[$part->location])) {   // Perform lazy evaluation
            $vars = $this->get_global_variables(&$state);  // if global is not defined
            $vars->evaluate_additional_variables($part->vars1);
            $state->localvars[$part->location] = $vars->get_variables();
        }   // Perform the evaluation only when the local variable does not exist before
        return new question_formulas_variables($state->localvars[$part->location]);
    }
    
    
    /// return the evaluated answer array (number will be converted to array). Throw on error
    function get_evaluated_answer($part, &$state) {
        if (!isset($state->evaluatedanswer[$part->location])) {   // Perform lazy evaluation
            $vars = $this->get_local_variables($part, &$state);
            $res = $vars->get_variables();
            $vars->evaluate_assignments_stack_recursively(array(array('_r', $part->answer)), $res, $tmpnames);
            $state->evaluatedanswer[$part->location] = is_array($res['_r']) ? $res['_r'] : array($res['_r']);
        }   // Perform the evaluation only when the local variable does not exist before
        return $state->evaluatedanswer[$part->location];
    }
    
    
    /// Override. A different grading scheme is used because we need to give a grade to each subanswer.
    function grade_responses(&$question, &$state, $cmoptions) {
        try {
            $this->rationalize_responses(&$question, &$state);      // may throw if the subqtext changed
            
            $unit = new answer_unit_conversion; // it is defined here for the possibility of reusing parsed default set
            $state->raw_grades = $state->last_graded->raw_grades;   // if no grading occurs, simply use last record
            $state->fractions = $state->last_graded->fractions;
            $state->anscorrs = $state->last_graded->anscorrs;
            $state->unitcorrs = $state->last_graded->unitcorrs;
            $state->trials = $state->last_graded->trials;
            foreach ($question->options->answers as $idx => $part)  if ($state->responses['subanum'] == -1 || $state->responses['subanum'] == $idx) {
                $tmf = $this->get_trial_mark_fraction($part, $state->trials[$idx]+1);
                if ($tmf[0] < 0)  continue;  // No further grading is allowed, which may be caused by browser resubmission
                $state->trials[$idx]++;
                list($state->anscorrs[$idx],$state->unitcorrs[$idx]) = $this->grade_responses_individually($part, $state, $state->responses, $unit);
                $state->fractions[$idx] = $state->anscorrs[$idx] * ($state->unitcorrs[$idx] ? 1 : (1-$part->unitpenalty));
                $raw_grade = $part->answermark * $state->fractions[$idx] * $tmf[0];
                $state->raw_grades[$idx] = max($raw_grade, $state->last_graded->raw_grades[$idx]);
            }
        } catch (Exception $e) {
            notify('Grading error! Probably result of incorrect import file or database corruption.');
            return false;// it should have no error when grading students question ...............
        }
        
        // The default additive penalty scheme is not used, so set penalty=0 and the raw_grade with penalty are directly computed
        $state->raw_grade = array_sum($state->raw_grades);
        $state->penalty = 0;

        // mark the state as graded
        $state->event = ($state->event ==  QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
        return true;
    }
    
    
    /// fill all 'missing' response by the default values and remove unwanted characters
    function rationalize_responses(&$question, &$state) {
        $number_regex = '[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?';
        $responses = &$state->responses;
        
        foreach ($question->options->answers as $i => $part)  foreach (range(0,$part->numbox) as $j) {
            $name = "${i}_$j";
            $ex = explode("\n", isset($responses[$name]) ? trim($responses[$name]) : $state->last_graded->responses[$name]);
            $responses[$name] = $ex[0];   // remove endline character and fill the missing responses by an empty string
            if (isset($responses["${i}_"])) {   // for a long answer box, always parse it into a number and unit, say, "0_0" and "0_1"
                $response_ex = explode("\n", $responses["${i}_"]);
                $tmp = explode("\n", preg_replace('/^'.$number_regex.'/', "$0\n", trim($response_ex[0]), 1));
                $responses["${i}_0"] = $tmp[0]; // It will be checked later whether tmp[0] is a number
                $responses["${i}_1"] = isset($tmp[1]) ? $tmp[1] : '';
            }   // the else case may occur if there is no further submission for answer $i, in which case we copy the "0_0" and "0_1" in above case
        }
        if (! ($question->options->extra->peranswersubmit == 1 && isset($responses['subanum']) &&
            $responses['subanum'] >= 0 && $responses['subanum'] < $question->options->numpart) )
            $responses['subanum'] = -1;   // negative means all answers are submitted at once, all answers are graded
    }
    
    
    /// grade the response and the unit together and return a single mark
    function grade_responses_individually($part, &$state, $responses, &$unit) {
        // parse the input into an objects with coordinates and postunit
        foreach (range(0,$part->numbox-1) as $j) {
            $r = $responses[$part->location."_".$j];
            if (!is_numeric($r))  return array(0,0);    // 0 marks will be given, if answer contains non-numeric value, maybe empty string
            $input->coordinates[] = floatval($r);;
        }
        $input->postunit = $responses[$part->location."_".$part->numbox];
        
        // check whether the unit part of the response is possibly correct
        global $basic_unit_conversion_rules;
        $unit->assign_default_rules($part->ruleid, $basic_unit_conversion_rules[$part->ruleid][1]);
        $unit->assign_additional_rules($part->otherrule);
        $res2 = $unit->check_convertibility($input->postunit, $part->postunit);
        $cfactor = $res2->cfactor;
        $unit_correct = $res2->convertible;
        // unit should have no effect (i.e. always true) if the coordinates are all 0
        $is_origin = true;
        foreach ($input->coordinates as $i => $v)  $is_origin = $is_origin && ($v == 0);
        if ($is_origin)  $unit_correct = true;
        
        // check the correctness of the coordinate part
        $ans = $this->get_evaluated_answer($part, &$state);
        if (count($input->coordinates) != count($ans))  return array(0,0);
        $vars = $this->get_local_variables($part, &$state);    // it contains the global and local variables before answer
        
        foreach ($input->coordinates as $idx => &$coord)  $coord *= $cfactor; // rescale response to match answer
        $this->add_special_correctness_variables($vars, $ans, $input->coordinates);
        // evaluated the set of local variables preceding the correctness expression
        $vars->evaluate_additional_variables($part->vars2);
        // evaluate the $correctness_expression to determine whether the question is correct
        $correctness = $vars->evaluate_numerical_expression($part->correctness);
        
        $answer_correct = min(max((float) $correctness, 0.0), 1.0);
        return array($answer_correct, $unit_correct ? 1 : 0);
    }
    
    
    /// add the set of special variables that may be useful to guage the correctness of the user input
    function add_special_correctness_variables(&$vars, $_a, $_r) {
        //$vars->add_variable('_r', $_r);
        //$vars->add_variable('_a', $_a);
        foreach ($_r as $idx => $coord)  $vars->add_variable('_'.$idx.'', $coord);
        $sum0 = $sum1 = $sum2 = $maxerr = $relerr = $norm_sqr = 0;
        foreach ($_a as $idx => $coord) {
            $norm_sqr += $coord*$coord;
            $diff[$idx] = $coord - $_r[$idx];
            //$match[$idx] = ($diff[$idx] == 0) ? 0 : 1;
            //$d1[$idx] = abs($diff[$idx]);
            $d2[$idx] = $diff[$idx]*$diff[$idx];
            $sum0 += ($diff[$idx] == 0) ? 0 : 1;
            $sum1 += abs($diff[$idx]);
            $sum2 += $d2[$idx];
            $maxerr = max($maxerr, abs($diff[$idx]));
        }
        //$vars->add_variable('_diff', $diff);
        //$vars->add_variable('_d', $d2);
        //$vars->add_variable('_wrong', $sum0);
        //$vars->add_variable('_sumerr', $sum1);
        $vars->add_variable('_err', sqrt($sum2));
        //$vars->add_variable('_maxerr', $maxerr);
        $vars->add_variable('_relerr', sqrt($sum2/$norm_sqr)); // division by zero when exact match!
    }
    
    
    /// compute the correct response for the given subquestion part
    function get_correct_responses_individually($part, &$state) {
        try {
            $res = $this->get_evaluated_answer($part, &$state);
        } catch (Exception $e)  { return null; }
        
        foreach (range(0,count($res)-1) as $j)  $responses[$part->location."_$j"] = $res[$j]; // coordinates
        $tmp = explode('=', $part->postunit, 2);
        $responses[$part->location."_".count($res)] = $tmp[0];  // postunit
        return $responses;
    }
    
    
    /// compute the correct responses of each subquestion, if any
    function get_correct_responses(&$question, &$state) {
        $responses = array();
        foreach ($question->options->answers as $part) {
            $tmp = $this->get_correct_responses_individually($part, &$state);
            if ($tmp == null)  return null;
            $responses = array_merge($responses, $tmp);
        }
        return $responses;
    }
    
    
    /// Define the equivalence of the responses of subquestions
    /*function compare_responses(&$question, $state, $teststate) {
        $res = true;
        foreach ($question->options->answers as $i => $part) {
            // In case of missing response, we assume that it is the same as old answer, i.e. don't check
            $names = array("${i}_");    // response["0_"] etc is not used when we have multiple box
            foreach (range(0,$part->numbox) as $j)  $names[] = "${i}_$j";
            foreach ($names as $name)  if (isset($state->responses[$name]))
                $res = $res && (trim($state->responses[$name]) === $teststate->responses[$name]);
        }
        return $res;
    }*/
    
    
    /// Return a summary string of student responses. Need to override because it prints the data...
    function response_summary($question, $state, $length = 80) {
        $responses = $this->get_actual_response($question, $state);
        $summaries = '';
        foreach ($question->options->answers as $idx => $part)  if ($state->responses['subanum'] == -1 || $state->responses['subanum'] == $idx) {
            $c = question_get_feedback_class($state->fractions[$idx]);
            $res = array();
            foreach (range(0,$part->numbox) as $j)  $res[] = $state->responses["${idx}_$j"]; // get the set of responses for this subquestion
            $summaries .= '<div class="'.$c.'">'.'<i>('.($idx+1).'.) </i> '.shorten_text(implode(" ",$res), $length).'</div>';
        }
        return $summaries;
    }
    
    
    
    
    
    /**
     * Imports the question from Moodle XML format.
     *
     * @param $data structure containing the XML data
     * @param $question question object to fill: ignored by this function (assumed to be null)
     * @param $format format class exporting the question
     * @param $extra extra information (not required for importing this question in this format)
     */
    function import_from_xml(&$data,&$question,&$format,&$extra) {
        // return if type in the data is not coordinate question
        $nodeqtype = $data['@']['type'];
        if ($nodeqtype != $this->name() && $nodeqtype != 'coordinates')  return false;
        // Import the common question headers and set the corresponding field
        $qo = $format->import_headers($data);
        $qo->qtype = $this->name();
        $extras = $this->subquestion_option_extras();
        foreach ($extras as $extra)
            $qo->$extra = $format->getpath($data, array('#',$extra,0,'#','text',0,'#'),'',true);
        
        // Loop over each answer block found in the XML
        $tags = $this->subquestion_answer_tags();
        $answers = $data['#']['answers'];  
        foreach($answers as $answer) {
            foreach($tags as $tag) {
                $qotag = &$qo->$tag;
                $qotag[] = $format->getpath($answer, array('#',$tag,0,'#','text',0,'#'),'0',false,($nodeqtype == 'coordinates') ? '' : 'error');
            }
        }
        $qo->defaultgrade = array_sum($qo->answermark); // make the defaultgrade consistent if not specified
        
        // Backward compatibility
        if ($nodeqtype == 'coordinates') {  // simple and dirty trick to import the old coordinates questions, works in most case
            echo '========== Import coordinates qtype to formulas qtype  ===========<br>'; // Mainly update the syntax, and new placeholder method
            $qo->varsrandom = preg_replace('/\{([#A-Za-z_]\w*)\}/', '${1}', $qo->varsrandom);   // remove bracket {} for variable
            $qo->varsrandom = preg_replace('/([0-9.eE+-]+)\s*~\s*([0-9.eE+-]+)\s*:\s*([0-9.eE+-]+)/', '${1}:${2}:${3}', $qo->varsrandom);  // new range syntax
            $qo->varsrandom = preg_replace('/(shuffle\s*)\((\s*[0-9.eE+-]+(\s*,\s*[0-9.eE+-]+)+\s*)\)/', '${1}([${2}])', $qo->varsrandom);  // shuffle requires range
            $qo->varsrandom = preg_replace('/\((\s*[0-9.eE+-]+(\s*,\s*[0-9.eE+-]+)+\s*)\)/', '[${1}]', $qo->varsrandom);    // new tuple syntax
            $qo->varsglobal = preg_replace('/\{([#A-Za-z_]\w*)\}/', '${1}', $qo->varsglobal);   // remove bracket {} for variable
            $qo->varsglobal = preg_replace('/(=\s*)\((\s*[0-9.eE+-]+(\s*,\s*[0-9.eE+-]+)+\s*)\)(\s*;)/', '${1}[${2}]$4', $qo->varsglobal);  // assign tuple
            $retrymarkseq = $format->getpath($data, array('#','retrymarkseq',0,'#','text',0,'#'),'',true);
            foreach (range(0,count($qo->answermark)-1) as $idx => $notused)  $qo->trialmarkseq[$idx] = (strlen(trim($retrymarkseq)) == 0) ? '1' : '1,'.$retrymarkseq;
            foreach (range(0,count($qo->answermark)-1) as $idx => $notused)  $qo->answertype[$idx] = 0;
            foreach($tags as $tag)  foreach ($qo->$tag as $idx => $notused) {
                $entries = &$qo->$tag;
                $entries[$idx] = preg_replace('/(=\s*)\((\s*[0-9.eE+-]+(\s*,\s*[0-9.eE+-]+)+\s*)\)(\s*;)/', '${1}[${2}]$4', $entries[$idx]);    // assign tuple
                $entries[$idx] = preg_replace('/\{([#A-Za-z_]\w*)\}/', '${1}', $entries[$idx]); // Variable in assignments do not require {} anymore
            }
            foreach ($qo->answer as $idx => $notused)  {
                $tmp = explode(';',$qo->answer[$idx]);    // Answer require tuple rather than ';', not work as expected
                if (count($tmp) > 1) {
                    foreach ($tmp as $i => $t)  $qo->vars1[$idx] .= "\nImportANS$i=$t;";
                    foreach ($tmp as $i => $t)  $tmp[$i] = 'ImportANS'.$i;
                    $qo->answer[$idx] = '['.implode(',',$tmp).']'; 
                }
            }
            $qo->questiontext = preg_replace('/\{#([A-Za-z0-9]+)\}(\[[0-9]+\])?/', '{#${1}${2}}', $qo->questiontext);   // change {a}[1] to {a[1]}
        }
        
        return $qo;
    }
    
    
    /**
     * Exports the question to Moodle XML format.
     *
     * @param $question question to be exported into XML format
     * @param $format format class exporting the question
     * @param $extra extra information (not required for exporting this question in this format)
     * @return text string containing the question data in XML format
     */
    function export_to_xml(&$question,&$format,&$extra) {
        $expout = '';
        $extras = $this->subquestion_option_extras();
        foreach ($extras as $extra)
            $expout .= "<$extra>".$format->writetext($question->options->extra->$extra)."</$extra>\n";
        
        $tags = $this->subquestion_answer_tags();
        foreach ($question->options->answers as $answer) {
            $expout .= "<answers>\n";
            foreach ($tags as $tag)
                $expout .= " <$tag>\n  ".$format->writetext($answer->$tag)." </$tag>\n";
            $expout .= "</answers>\n";
        }
        return $expout;
    }
    
    
    /**
     * Backup the data in the question to a backup file.
     *
     * This function is used by question/backuplib.php to create a copy of the data
     * in the question so that it can be restored at a later date. The method writes
     * all the supplementary coordinate data, including the answers of the subquestions.
     *
     * @param $bf the backup file to write the information to
     * @param $preferences backup preferences in effect (not used)
     * @param $questionid the ID number of the question being backed up
     * @param $level the indentation level of the data being written
     * 
     * @return bool true if the backup was successful, false if it failed.
     */
    function backup($bf,$preferences,$questionid,$level=6) {
        $question->id = $questionid;
        $this->get_question_options($question); // assume no error
        
        // Start tag of data
        $status = true;
        $status = $status && fwrite ($bf,start_tag('FORMULAS',$level,true));
        $extras = $this->subquestion_option_extras();
        foreach ($extras as $extra)
            fwrite ($bf,full_tag(strtoupper($extra), $level+1, false, $question->options->extra->$extra));
        
        // Iterate over each answer and write out its fields
        $tags = $this->subquestion_answer_tags();
        foreach ($question->options->answers as $var) {
            $status = $status && fwrite ($bf,start_tag('ANSWERS',$level+1,true));
            foreach ($tags as $tag)
                fwrite ($bf, full_tag(strtoupper($tag), $level+2, false, $var->$tag));
            $status = $status && fwrite ($bf,end_tag('ANSWERS',$level+1,true));
        }
        
        // End tag of data
        $status = $status && fwrite ($bf,end_tag('FORMULAS',$level,true));
        return $status;
    }
    
    
    /**
     * Restores the data in a backup file to produce the original question.
     *
     * This method is used by question/restorelib.php to restore questions saved in
     * a backup file to the database. It reads the file directly and writes the information
     * straight into the database.
     *
     * @param $old_question_id the original ID number of the question being restored
     * @param $new_question_id the new ID number of the question being restored
     * @param $info the XML parse tree containing all the restore information
     * @param $restore information about the current restore in progress
     * 
     * @return bool true if the backup was successful, false if it failed.
     */
    function restore($old_question_id,$new_question_id,$info,$restore) {
        $data = $info['#']['FORMULAS'][0];
        $qo = new stdClass;
        $qo->id          = $new_question_id;
        $qo->qtype       = $this->name();
        $extras = $this->subquestion_option_extras();
        foreach ($extras as $extra)
            $qo->$extra = backup_todb($data['#'][strtoupper($extra)]['0']['#']);
        
        // Loop over each answer block found in the XML
        $tags = $this->subquestion_answer_tags();
        $answers = $data['#']['ANSWERS'];  
        foreach($answers as $answer) {
            foreach($tags as $tag) {
                $qotag = &$qo->$tag;
                $qotag[] = backup_todb($answer['#'][strtoupper($tag)]['0']['#']);
            }
        }
        return is_bool($this->save_question_options($qo)) ? true : false;
    }
    
    
    
    
    
    /// It checks the basic error as well as the formula error by evaluating one instantiation.
    function validate($data) {
        $form = (object)$data;
        $errors = array();
        
        $answerschecked = $this->check_and_filter_answers($form);
        if (isset($answerschecked->errors))  $errors = array_merge($errors, $answerschecked->errors);
        $validanswers = $answerschecked->answers;
        
        foreach ($validanswers as $idx => $part) {
            if ($part->unitpenalty < 0 || $part->unitpenalty > 1)
                $errors["unitpenalty[$idx]"] = get_string('error_unitpenalty','qtype_formulas');
            try {
                $this->get_trial_mark_fraction($part, 0);
            } catch (Exception $e) {
                $errors["trialmarkseq[$idx]"] = $e->getMessage();
            }
            try {
                $pattern = '\{(_[0-9u][0-9]*)(:[^{}]+)?\}';
                preg_match_all('/'.$pattern.'/', $part->subqtext, $matches);
                $boxes = array();
                foreach ($matches[1] as $j => $match)  if (array_key_exists($match, $boxes))
                    throw new Exception(get_string('error_subqtext_placeholder_duplicate','qtype_formulas'));
                else
                    $boxes[$match] = 1;
            } catch (Exception $e) {
                $errors["subqtext[$idx]"] = $e->getMessage().$idx;
            }
        }
        
        $placeholdererrors = $this->check_placeholder($form->questiontext, $validanswers);
        $errors = array_merge($errors, $placeholdererrors);
        
        $instantiationerrors = $this->validate_instantiation($data, &$validanswers);
        $errors = array_merge($errors, $instantiationerrors); 
        
        return (object)array('errors' => $errors, 'answers' => $validanswers);
    }
    
    
    /// Validating the data from the client, and return errors. If no errors, the $validanswers should be appended by numbox variables
    function validate_instantiation($data, &$validanswers) {
        global $basic_unit_conversion_rules;
        $form = (object)$data;
        $errors = array();
        
        try {
            $vars = new question_formulas_variables();
            $vars->parse_random_variables($form->varsrandom);
            $vars->instantiate_random_variables(); // instantiate a set of random variable
            $state->randomvars = $vars->get_variables();
        } catch (Exception $e) {
            $errors["varsrandom"] = $e->getMessage();
            return $errors;
        }
        
        try {
            $vars->evaluate_additional_variables($form->varsglobal);
        } catch (Exception $e) {
            $errors["varsglobal"] = $e->getMessage();
            return $errors;
        }
        $state->globalvars = $vars->get_variables();
        
        /// Attempt to compute the answer so that it can see whether it is wrong
        foreach ($validanswers as $idx => $ans) {
            $ans->location = $idx;
            $vars = new question_formulas_variables($state->globalvars);
            $unitcheck = new answer_unit_conversion;
            try {
                $unitcheck->parse_targets($ans->postunit);
            } catch (Exception $e) {
                $errors["postunit[$idx]"] = get_string('error_validation_parse_rule','qtype_formulas') . $e->getMessage();
            }
            try {
                $unitcheck->assign_additional_rules($ans->otherrule);
                $unitcheck->reparse_all_rules();
            } catch (Exception $e) {
                $errors["otherrule[$idx]"] = get_string('error_validation_parse_rule','qtype_formulas') . $e->getMessage();
            }
            try {
                $entry = $basic_unit_conversion_rules[$ans->ruleid];
                if ($entry === null || $entry[1] === null)  throw new Exception(get_string('error_validation_ruleid','qtype_formulas'));
                $unitcheck->assign_default_rules($ans->ruleid, $entry[1]);
                $unitcheck->reparse_all_rules();
            } catch (Exception $e) {
                $errors["ruleid[$idx]"] = $e->getMessage();
            }
            try {
                $vars->evaluate_additional_variables($ans->vars1);
            } catch (Exception $e) {
                $errors["vars1[$idx]"] = get_string('error_validation_eval','qtype_formulas') . $e->getMessage();
                continue;
            }
            try {
                $input = $this->get_evaluated_answer($ans, &$state);
                $ans->numbox = count($input);   // here we set the number of 'coordinate' which is used to display number of answer box
                } catch (Exception $e) {
                $errors["answer[$idx]"] = get_string('error_validation_eval','qtype_formulas') . $e->getMessage();
                continue;
            }
            try {
                $this->add_special_correctness_variables($vars, $input, $input);
                $vars->evaluate_additional_variables($ans->vars2);
            } catch (Exception $e) {
                $errors["vars2[$idx]"] = get_string('error_validation_eval','qtype_formulas') . $e->getMessage();
                continue;
            }
            try {
                $responses = $this->get_correct_responses_individually($ans, &$state);
                $correctness = $this->grade_responses_individually($ans, &$state, $responses, &$unitcheck);
            } catch (Exception $e) {
                $errors["correctness[$idx]"] = get_string('error_validation_eval','qtype_formulas') . $e->getMessage();
                continue;
            }
        }
        
        return $errors;
    }
    
    
    /**
     * Check that all required fields have been filled and return the filtered classes of the answers.
     * 
     * @param $form all the input form data
     * @return an object with a field 'answers' containing valid answers. Otherwise, the 'errors' field will be set
     */
    function check_and_filter_answers($form) {
        $tags = $this->subquestion_answer_tags();
        $res = (object)array('answers' => array());
        foreach ($form->answermark as $i=>$a) {
            if (strlen(trim($form->answermark[$i])) == 0)
                continue;   // if no mark, then skip this answers
            if (floatval($form->answermark[$i]) <= 0)
                $res->errors["answermark[$i]"] = get_string('error_mark','qtype_formulas');
            $skip = false;
            if (strlen(trim($form->answer[$i])) == 0) {
                $res->errors["answer[$i]"] = get_string('error_answer_missing','qtype_formulas');
                $skip = true;
            }
            if (strlen(trim($form->correctness[$i])) == 0) {
                $res->errors["correctness[$i]"] = get_string('error_correctness_missing','qtype_formulas');
                $skip = true;
            }
            if ($skip)  continue;   // if no answer or correctness conditions, it cannot check other parts, so skip
            $res->answers[$i] = (object)array('questionid' => $form->id);   // create an object of answer with the id
            foreach ($tags as $tag)  $res->answers[$i]->{$tag} = trim($form->{$tag}[$i]);
        }
        if (count($res->answers) == 0)
            $res->errors["answermark[0]"] = get_string('error_no_answer','qtype_formulas');
        return $res;
    }
    
    
    /**
     * Split and reorder the main question by the placeholders. The check_placeholder() should be called before
     * 
     * @param string $questiontext The input question text containing a set of placeholder
     * @param array $answers Array of answers, containing the placeholder name  (must not empty)
     * @return Return the object with field answerorders, pretexts and posttext.
     */
    function create_subquestion_structure($questiontext, $answers) {
        $locations = array();   // store the (scaled) location of the *named* placeholder in the main text
        foreach ($answers as $idx => $answer)  if (strlen($answer->placeholder) != 0)
            $locations[] = 1000*strpos($questiontext, '{'.$answer->placeholder.'}') + $idx; // store the pair (location, idx)
        sort($locations);       // performs stable sort of location and answerorder pair
        
        $ss = new stdClass();
        foreach ($locations as $i => $location) {
            $answerorder = $location%1000;
            $ss->answerorders[] = $answerorder; // store the new location of the placeholder in the main text
            list($ss->pretexts[$i],$questiontext) = explode('{'.$answers[$answerorder]->placeholder.'}', $questiontext);
        }
        foreach ($answers as $idx => $answer)  if (strlen($answer->placeholder) == 0) { // add the empty placeholder at the end
            $ss->answerorders[] = $idx;
            $ss->pretexts[] = $questiontext;
            $questiontext = '';
        }
        $ss->posttext = $questiontext;  // add the post-question text, if any
        
        return $ss;
    }
    
    
    /// check whether the placeholder in the $answers is correct and compatible with $questiontext
    function check_placeholder($questiontext, $answers) {
        $placeholder_format = '#\w+';
        $placeholders = array();
        foreach ($answers as $idx => $answer) {
            if ( strlen($answer->placeholder) == 0 )  continue; // no error for empty placeholder
            $errstr = '';
            if ( strlen($answer->placeholder) >= 40 ) 
                $errstr .= get_string('error_placeholder_too_long','qtype_formulas');
            if ( !preg_match('/^'.$placeholder_format.'$/', $answer->placeholder) )
                $errstr .= get_string('error_placeholder_format','qtype_formulas');
            if ( array_key_exists($answer->placeholder, $placeholders) )
                $errstr .= get_string('error_placeholder_sub_duplicate','qtype_formulas');
            $placeholders[$answer->placeholder] = true;
            $count = substr_count($questiontext, '{'.$answer->placeholder.'}');
            if ($count<1)
                $errstr .= get_string('error_placeholder_missing','qtype_formulas');
            if ($count>1)
                $errstr .= get_string('error_placeholder_main_duplicate','qtype_formulas');
            if (strlen($errstr) != 0)  $errors["placeholder[$idx]"] = $errstr;
        }
        return isset($errors) ? $errors : array();
    }
    
}

// Register this question type with the system.
question_register_questiontype(new question_formulas_qtype());
?>
